/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package viewer.mortgage;

import Household.Household;
import java.awt.geom.NoninvertibleTransformException;
import java.util.*;


import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.io.*;
import java.awt.BasicStroke;

import java.util.logging.Level;
import java.util.logging.Logger;
import java.awt.RenderingHints;


import MortgageCore.*;
import Bank.Bank;
import data.LocalData;
import geography.Geography.City;

import viewer.common.Trans2D;
import simframe.simulation.SimViewer;
import simframe.simulation.Metric;
import simframe.simulation.SwarmMetric;

/**
 *
 * @author Ken Karakotsis
 */
class MortgageCoreView extends javax.swing.JPanel implements SimViewer {

    private static class MapStats {

        Map<SwarmMetric<Household>, Double> metrics = new HashMap<SwarmMetric<Household>, Double>();
        Map<SwarmMetric<Household>, Double> allowed = new HashMap<SwarmMetric<Household>, Double>();
        double size;
    }

    enum MapMode {
        Object,
        Bubble,
        StateHeat
    }

    // screen size
    private static final int width = 900;
    private static final int height = 600;

    // scaling for mapping lat longs to coord space
    private static final double xScale = 16; //making xScale larger stretchs US east west
    private static final double yScale = 24;
    private static final long serialVersionUID = 1L;
    private java.awt.Rectangle rubberband = null;
    private final SavingsSim sim;
    Trans2D trans;
    Map<String, List<java.awt.Polygon>> states;
    final private List<HouseholdMCView> householdViews = new ArrayList<HouseholdMCView>();
    final private List<BankMCView> bankViews = new ArrayList<BankMCView>();
    final private Map<City, MapStats> citySize = new HashMap<City, MapStats>();
    final private Map<SwarmMetric<Household>, Color> metricColor = new HashMap<SwarmMetric<Household>, Color>();
    final private Map<String, Color> stateColor = new HashMap<String, Color>();
    private List<Household> hhs = new ArrayList<Household>();
    private int[] diams = new int[]{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
    private java.awt.geom.AffineTransform tx = new java.awt.geom.AffineTransform();
    private double minLat = Double.MAX_VALUE;
    private double maxLat = -Double.MAX_VALUE;
    private double minLong = Double.MAX_VALUE;
    private double maxLong = -Double.MAX_VALUE;
    private double totalSize = 0;
    private MapMode mapMode = MapMode.Object;

    // for object map view
    private boolean drawHomeowners;
    private boolean drawBanks;
    private boolean drawRenters;

    void zoom(double val) {

        final Point p = new Point();
        final Point q = new Point();

        p.x = (int) (this.getBounds().getCenterX() / 2);
        p.y = (int) (this.getBounds().getCenterY() / 2);

        try {
            tx.inverseTransform(p, q);
        } catch (NoninvertibleTransformException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }

        java.awt.geom.AffineTransform t0 = new java.awt.geom.AffineTransform();
        t0.setToTranslation(-q.x, -q.y);

        java.awt.geom.AffineTransform scale = new java.awt.geom.AffineTransform();

        scale.setToScale(val, val);

        scale.concatenate(t0);

        java.awt.geom.AffineTransform t1 = new java.awt.geom.AffineTransform();
        t1.setToTranslation(q.x, q.y);

        t1.concatenate(scale);

        tx.concatenate(t1);

        if (this.mapMode == MapMode.Object) {
            resetObjectMap();
        }
    }

    void setDrawHomowners(boolean b) {
        drawHomeowners = b;
    }

    void setDrawBanks(boolean b) {
        drawBanks = b;
    }

    void setDrawRenters(boolean b) {
        drawRenters = b;
    }

    boolean getDrawHomowners(boolean b) {
        return drawHomeowners;
    }

    boolean getDrawBanks(boolean b) {
        return drawBanks;
    }

    boolean getDrawRenters(boolean b) {
        return drawRenters;
    }

    Trans2D getTrans2D() {
        return trans;
    }

    void setMapMode(MapMode val) {
        mapMode = val;

        if (mapMode == MapMode.Object) {
            addMouseListener(selectObjects);
        } else {
            removeMouseListener(selectObjects);
        }
    }

    void addMetric(SwarmMetric<Household> metric) {

        for (Household hh : hhs) {
            MapStats stat = citySize.get(hh.getCity());

            stat.metrics.put(metric, 0.0);
        }
    }

    void addDisplay(SwarmMetric<Household> metric, Color col) {

        if (metricColor.containsKey(metric)) {
            metricColor.remove(metric);
        }

        metricColor.put(metric, new Color(col.getRed(), col.getGreen(), col.getBlue(), 50));

    }

    void removeDisplay(SwarmMetric<Household> metric) {

        if (metricColor.containsKey(metric)) {
            metricColor.remove(metric);
        }
    }

    void updateTrans2D(double x0, double y0, double x1, double y1) {

        double scale = Math.max((maxLong - minLong) / (x1 - x0),
                (maxLat - minLat) / (y1 - y0));


        trans = new viewer.common.Trans2D(x0, y0, scale * xScale, scale * yScale);
    }

    void setRubberBand(int x, int y, int w, int h) {
        rubberband = new java.awt.Rectangle(x, y, w, h);
    }

    void clearRubberBand() {
        rubberband = null;
    }

    MortgageCoreView(SavingsSim sim) {
        this.sim = sim;

        addMouseListener(clickToDrag);
        this.addMouseMotionListener(clickToDrag);

        addMouseListener(selectObjects);

        try {
            getMaxLatLong();
        } catch (IOException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }

        updateTrans2D(minLong, minLat, maxLong, maxLat);

        try {
            readUsShapeFile();
        } catch (IOException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }


        // this.setBounds(0, 0, width, height);

        // get view boundaries
        resetHeatMap();

        resetObjectMap();
        drawHomeowners = true;
        drawBanks = true;
        drawRenters = true;
    }

    MortgageCoreView(SavingsSim sim, double x0, double y0, double x1, double y1) {
        this.sim = sim;

        addMouseListener(clickToDrag);
        this.addMouseMotionListener(clickToDrag);
        addMouseListener(selectObjects);


        updateTrans2D(x0, y0, x1, y1);

        this.setBounds(0, 0, width, height);

        resetHeatMap();

        resetObjectMap();

        try {
            readUsShapeFile();
        } catch (IOException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    java.awt.event.MouseAdapter clickToDrag = new java.awt.event.MouseAdapter() {

        int curX = 0;
        int curY = 0;

       @Override
       public void mousePressed(java.awt.event.MouseEvent evt) {
            curX = evt.getX();
            curY = evt.getY();
        }

        @Override
        public void mouseDragged(java.awt.event.MouseEvent evt) {

            int x = evt.getX();
            int y = evt.getY();

            java.awt.geom.AffineTransform t = new java.awt.geom.AffineTransform();
            t.setToTranslation(x - curX, y - curY);

            tx.concatenate(t);

            curX = x;
            curY = y;

            repaint();

        }

        @Override
        public void mouseReleased(java.awt.event.MouseEvent evt) {

            if (mapMode == MapMode.Object) {

                resetObjectMap();

                repaint();
            }

        }
    };
    java.awt.event.MouseAdapter selectObjects = new java.awt.event.MouseAdapter() {

        @Override
        public void mouseClicked(java.awt.event.MouseEvent evt) {
            simCanvasMouseClicked(evt);
        }
    };

    private void simCanvasMouseClicked(java.awt.event.MouseEvent evt) {
        Point p = evt.getPoint();

        //        check for multiple select
        boolean allSelected = false;
        boolean multipleSelected = false;
        int modifiers = evt.getModifiersEx();
        int s = java.awt.event.InputEvent.SHIFT_DOWN_MASK;
        int ctrl = java.awt.event.InputEvent.CTRL_DOWN_MASK;
        if ((modifiers & s) == s) {
            multipleSelected = true;
        }

        if (multipleSelected) {
            if ((modifiers & ctrl) == ctrl) {
                allSelected = true;
                multipleSelected = false;
            } else {
                allSelected = false;
            }
        }

        checkHit(p, multipleSelected, allSelected);
    }

    Metric.Constrain<Household> getHHConstraint() {
        return new Metric.Constrain<Household>() {

            // get view boundaries
            private Trans2D.Vec2D origin = trans.invMap(new Point(0, 0));
            private Trans2D.Vec2D corner = trans.invMap(new Point(width, height));

            public boolean allowed(Household hh) {
                if (hh.getLoc().getX() > origin.x &&
                        hh.getLoc().getX() < corner.x &&
                        hh.getLoc().getY() > origin.y &&
                        hh.getLoc().getY() < corner.y) {
                    return true;
                }
                return false;
            }
        };
    }

    public void reset() {
//        householdViews.clear();
//        bankViews.clear();
//        citySize.clear();
//        hhs.clear();
//
//        try {
//            readUsShapeFile();
//        } catch (IOException ex) {
//            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
//        }
//
//        // get view boundaries
//        Trans2D.Vec2D origin = trans.invMap(new Point(0, 0));
//        Trans2D.Vec2D corner = trans.invMap(new Point(width, height));
//
//
//        resetHeatMap(origin, corner);
//
//        resetObjectMap(origin, corner);
    }

    private void resetStateMap() {
    }

    private void resetHeatMap() {

        Metric.Constrain<Household> rule = getHHConstraint();

        totalSize = sim.households.size();

        for (Household hh : sim.households) {

            if (rule.allowed(hh)) {

                hhs.add(hh);

                if (!citySize.containsKey(hh.getCity())) {
                    citySize.put(hh.getCity(), new MapStats());
                }

                MapStats stat = citySize.get(hh.getCity());

                stat.size++;
            }
        }
    }

    private void resetObjectMap() {

        java.awt.Rectangle rect = this.getBounds();
        Point p = rect.getLocation();

        try {
            tx.inverseTransform(p, p);
        } catch (NoninvertibleTransformException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }

        Trans2D.Vec2D origin = trans.invMap(p);

        if (rect.getWidth() > 0 && rect.getHeight() > 0) {
            p.x = (int) rect.getWidth();
            p.y = (int) rect.getHeight();
        } else {
            p.x = Integer.MAX_VALUE;
            p.y = Integer.MAX_VALUE;
        }


        try {
            tx.inverseTransform(p, p);
        } catch (NoninvertibleTransformException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }

        Trans2D.Vec2D corner = trans.invMap(p);

        householdViews.clear();

        int total = 0;
        for (Household hh : sim.households) {

            if (hh.getLoc().getX() > origin.x &&
                    hh.getLoc().getX() < corner.x &&
                    hh.getLoc().getY() > origin.y &&
                    hh.getLoc().getY() < corner.y) {
                householdViews.add(new HouseholdMCView(hh, trans));
                ++total;

                if (total > 1000) {
                    break;
                }
            }
        }

        for (Bank bank : sim.banks) {
            if (bank.getLoc().getX() > origin.x &&
                    bank.getLoc().getX() < corner.x &&
                    bank.getLoc().getY() > origin.y &&
                    bank.getLoc().getY() < corner.y) {
                bankViews.add(new BankMCView(bank, trans));
            }
        }
    }

    @Override
    public void paintComponent(Graphics gIn) {

        super.paintComponent(gIn);

        java.awt.Graphics2D g = (java.awt.Graphics2D) gIn;

        g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);

        g.transform(tx);

        g.setBackground(Color.white);

        drawUS(g);



        switch(mapMode) {
            case Bubble:
                 drawBubbleMap(g);
                 break;
            case Object:
                 drawObjectMap(g);
                 break;
            case StateHeat:
                drawStates(g);
                break;
        }

        if (rubberband != null) {
            g.setColor(Color.red);
            g.drawRect(rubberband.x, rubberband.y, rubberband.width, rubberband.height);
        }
    }

    public void update(long t) {


        switch(mapMode) {
            case Bubble:
                  updateBubbleMap(t);
                 break;
            case Object:
                // Nothing to do
                 break;
            case StateHeat:
                updateStateMap(t);
                break;
        }

        repaint();
    }

    private void updateStateMap(long t) {
    }

    private void updateBubbleMap(long t) {

        for (MapStats stat : citySize.values()) {
            stat.metrics.clear();
            stat.allowed.clear();
        }

        synchronized (metricColor) {
            for (SwarmMetric<Household> metric : metricColor.keySet()) {

                Metric.Constrain<Household> constraint = metric.getConstraint();

                for (Household hh : hhs) {

                    MapStats stat = citySize.get(hh.getCity());

                    double curVal = 0;
                    double curNum = 0;

                    if (stat.metrics.containsKey(metric)) {
                        curVal = stat.metrics.get(metric);
                    }

                    if (constraint != null && stat.allowed.containsKey(metric)) {
                        curNum = stat.metrics.get(metric);
                    }

                    if (constraint != null) {

                        if (constraint.allowed(hh)) {
                            stat.allowed.put(metric, curNum + 1);
                            double val = metric.eval(hh);
                            stat.metrics.put(metric, curVal + val);
                        }
                    } else {

                        double val = metric.eval(hh);
                        stat.metrics.put(metric, curVal + val);
                    }
                }
            }
        }
    }

    private void drawBubbleMap(java.awt.Graphics2D g) {

        this.setBackground(Color.white);

        synchronized (metricColor) {
            for (City city : citySize.keySet()) {

                Trans2D.Vec2D v = new Trans2D.Vec2D(city.GetLongitude(), city.GetLatitude());

                Point p = trans.map(v);
                MapStats stat = citySize.get(city);

                int offset = 0;

                for (SwarmMetric<Household> metric : metricColor.keySet()) {
                    if (!stat.metrics.containsKey(metric)) {
                        break;
                    }

                    double perCapita = stat.metrics.get(metric) / stat.size;

                    int diam = diams[diams.length - 1];
                    double[] range = metric.getNumericalRange();

                    if (range != null && range.length > 0) {

                        for (int ii = 0; ii < range.length && ii < diams.length; ++ii) {
                            diam = diams[ii];
                            if (perCapita <= range[ii]) {

                                break;
                            }
                        }
                    } else {
                        diam = 1;
                        System.out.print("No range set for " + metric.getName());
                    }

                    int alpha = (int) (100.0 * diam / diams[diams.length - 1]);

                    Color col = metricColor.get(metric);
                    Color newColor = new Color(col.getRed(), col.getGreen(), col.getBlue(), alpha);

                    g.setColor(newColor);

                    // diam realted to size
                    // double  cityScale = 1000.0;
                    // diam =  5 + (int) (30 * alpha/225);

                    g.fillOval(offset + (p.x - diam / 2), offset + (p.y - diam / 2), diam, diam);
                    offset++;
                }
            }
        }
    }

    private void drawObjectMap(java.awt.Graphics2D g) {

        float val = 0.05f;

        Color c = new Color(val, val, val);

        double scale = g.getTransform().getScaleX();
        int size = (int) (MCMapObj.refDiameter / scale + 0.5);    // add 0.5 to adjust for roundoff error

        this.setBackground(c);

        for (HouseholdMCView hhv : householdViews) {
            hhv.drawMapImage(g, scale, drawHomeowners, drawRenters);
        }

        int ptsize = (int) (16 / scale);

        if (ptsize < 8) {
            ptsize = 8;
        }

        if (drawBanks) {
            String font = "Ariel-" + Integer.toString(ptsize);
            g.setFont(java.awt.Font.decode(font));
            for (BankMCView bank : bankViews) {
                bank.drawMapImage(g, scale);
            }
        }

    }

    void checkHit(Point p, boolean multipleSelected, boolean allSelected) {

        // check for hit from mouseclick
        // first the households
        Point q = new Point(0, 0);
        try {
            tx.inverseTransform(p, q);
        } catch (NoninvertibleTransformException ex) {
            Logger.getLogger(MortgageCoreView.class.getName()).log(Level.SEVERE, null, ex);
        }

        double scale = tx.getScaleX();

        int size = (int) (MCMapObj.refDiameter / scale);

        if (drawBanks) {
            for (BankMCView b : this.bankViews) {
                if (b.hit(q, scale, multipleSelected, allSelected) && !multipleSelected && !allSelected) {
                    repaint();
                    return;
                }
            }
        }


        for (HouseholdMCView hhv : this.householdViews) {
            if (hhv.hit(q, scale, multipleSelected, drawHomeowners, drawRenters) && !multipleSelected) {
                break;
            }
        }
        repaint();
    }

    void showHomes(boolean show) {
        for (HouseholdMCView hhv : this.householdViews) {
            hhv.setShowHome(show);
        }
    }

    private void getMaxLatLong() throws IOException {

        InputStream in_stream = LocalData.GetInputStream(LocalData.Input.US_SHAPE_FILE);
        InputStreamReader is_reader = new InputStreamReader(in_stream);
        BufferedReader reader = new BufferedReader(is_reader);

        String line;
        while ((line = reader.readLine()) != null) {

            String[] coords = line.split(",");

            String stateCode = coords[0];

            if ((stateCode.compareToIgnoreCase("Alaska") != 0) &&
                    (stateCode.compareToIgnoreCase("Hawaii") != 0)) {


                for (int index = 1; index < coords.length; index += 2) {
                    double x = Double.parseDouble(coords[index]);
                    double y = Double.parseDouble(coords[index + 1]);

                    minLat = Math.min(minLat, y);
                    maxLat = Math.max(maxLat, y);

                    minLong = Math.min(minLong, x);
                    maxLong = Math.max(maxLong, x);

                }
            }
        }

        reader.close();
        is_reader.close();
        in_stream.close();
    }

    private void readUsShapeFile() throws IOException {

        Trans2D.Vec2D origin = trans.invMap(new Point(0, 0));
        Trans2D.Vec2D corner = trans.invMap(new Point(width, height));

        states = new HashMap<String, List<java.awt.Polygon>>();

        InputStream in_stream = LocalData.GetInputStream(LocalData.Input.US_SHAPE_FILE);
        InputStreamReader is_reader = new InputStreamReader(in_stream);
        BufferedReader reader = new BufferedReader(is_reader);

        String line;
        while ((line = reader.readLine()) != null) {

            String[] coords = line.split(",");

            String stateCode = coords[0];

            List<java.awt.Polygon> polys;

            if (states.containsKey(stateCode)) {
                polys = states.get(stateCode);
            } else {
                polys = new ArrayList<java.awt.Polygon>();
                states.put(stateCode, polys);
            }

            java.awt.Polygon poly = new java.awt.Polygon();


            boolean usePoly = false;
            for (int index = 1; index < coords.length; index += 2) {
                double x = Double.parseDouble(coords[index]);
                double y = Double.parseDouble(coords[index + 1]);

                if (!usePoly && x > origin.x &&
                        x < corner.x &&
                        y > origin.y &&
                        y < corner.y) {

                    usePoly = true;
                }

                Point p = trans.map(new Trans2D.Vec2D(x, y));

                poly.addPoint(p.x, p.y);
            }

            if (usePoly) {
                polys.add(poly);
            }
        }
        reader.close();
        is_reader.close();
        in_stream.close();
    }

    private void drawUS(java.awt.Graphics2D g) {

        Color lineColor = new Color(0.616f, 0.784f, 0.776f, 1);
        g.setColor(lineColor);
        // set the line characteristics
        BasicStroke bs = new BasicStroke(1.0f, java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL);
        g.setStroke(bs);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        for (String stateCode : states.keySet()) {
            // pick a color for the state

            for (java.awt.Polygon poly : states.get(stateCode)) {

                g.drawPolygon(poly);
            }
        }

    }

    private void drawStates(java.awt.Graphics2D g) {

        // set the line characteristics
        BasicStroke bs = new BasicStroke(1.0f, java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL);
        g.setStroke(bs);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        for (String stateCode : states.keySet()) {
            // pick a color for the state

            Color col = this.stateColor.get(stateCode);

            if (col != null) {
                 g.setColor(col);
                  for (java.awt.Polygon poly : states.get(stateCode)) {

                    g.fillPolygon(poly);
                }
            }
        }

    }
}
